package model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class Tracker {

	private File file;
	private Collection<Node> nodes;
	private Map<FilePiece, Collection<Node>> completedPieceMap;
	private Map<FilePiece, Collection<Node>> workingPieceMap;
	private Collection<NodeConnection> connections;
	private int updates;

	public Tracker() {
		this.file = new File();
		nodes = new ArrayList<Node>();
		completedPieceMap = new HashMap<FilePiece, Collection<Node>>();
		workingPieceMap = new HashMap<FilePiece, Collection<Node>>();
		connections = new ArrayList<NodeConnection>();
	}
	
	public void removeNode(int nodeNumber){
		Iterator<Node> iter = nodes.iterator();
		while (iter.hasNext()) {
			Node node = iter.next();
			if (node.getNumber() == nodeNumber) {
				notifyNodeLeaving(node, true);
				iter.remove();
			}
		}
	}

	public void notifyNodeLeaving(Node node, boolean duringUpdate) {
		if (!duringUpdate)
			nodes.remove(node);
		for (FilePiece filePiece : node.getCompletedFilePieces()) {
			completedPieceMap.get(filePiece).remove(node);
		}
		for (FilePiece filePiece : node.getWorkingFilePieces()) {
			workingPieceMap.get(filePiece).remove(node);
		}
		Iterator<NodeConnection> iter = connections.iterator();
		while (iter.hasNext()) {
			NodeConnection connection = iter.next();
			if (connection.getUpNode() == node
					|| connection.getDownNode() == null) {
				connection.notifyNodeDisconnected(node);
				iter.remove();
			}
		}
	}

	public void addNode(Node node) {
		nodes.add(node);
		for (FilePiece piece : node.getCompletedFilePieces()) {
			if (!completedPieceMap.containsKey(piece)) {
				completedPieceMap.put(piece, new ArrayList<Node>());
			}
			completedPieceMap.get(piece).add(node);
		}
	}

	public void requestNewFilePiece(Node node) {
		List<FilePiece> rareAvailablePieces = getRarestAvailableFilePieces();
		for (FilePiece rarePiece : rareAvailablePieces) {
			if (!node.isAssociatedWithPiece(rarePiece)) {
				Node uploader = getBestUploader(completedPieceMap
						.get(rarePiece));
				double maxBandwidth = (uploader.upBandwidthRemaining() < node
						.downBandwidthRemaining()) ? uploader
						.upBandwidthRemaining() : node.downBandwidthRemaining();
				if (maxBandwidth > 0) {
					NodeConnection newConnection = new NodeConnection(this,
							uploader, node, rarePiece, maxBandwidth);
					connections.add(newConnection);
					if (!workingPieceMap.containsKey(rarePiece)) {
						workingPieceMap.put(rarePiece, new ArrayList<Node>());
					}
					workingPieceMap.get(rarePiece).add(node);
					break;
				}
			}
		}
	}

	private Node getBestUploader(Collection<Node> uploaderNodes) {
		Node bestUploaderNode = null;
		for (Node node : uploaderNodes) {
			if (bestUploaderNode == null) {
				bestUploaderNode = node;
			} else if (node.upBandwidthRemaining() > bestUploaderNode
					.upBandwidthRemaining()) {
				bestUploaderNode = node;
			}
		}

		return bestUploaderNode;
	}

	private List<FilePiece> getRarestAvailableFilePieces() {
		List<PieceNodeNumberPair> availablePieces = new ArrayList<PieceNodeNumberPair>();
		for (FilePiece filePiece : completedPieceMap.keySet()) {
			int numberOfNodesWithPiece = completedPieceMap.get(filePiece)
					.size();
			if (numberOfNodesWithPiece != 0) {
				if (workingPieceMap.containsKey(filePiece)) {
					availablePieces.add(new PieceNodeNumberPair(filePiece,
							numberOfNodesWithPiece, workingPieceMap.get(
									filePiece).size()));
				} else {
					availablePieces.add(new PieceNodeNumberPair(filePiece,
							numberOfNodesWithPiece, 0));
				}
			}
		}

		Collections.sort(availablePieces);
		List<FilePiece> rarePieceList = new ArrayList<FilePiece>();
		for (PieceNodeNumberPair pair : availablePieces) {
			rarePieceList.add(pair.getPiece());
		}

		return rarePieceList;
	}

	private class PieceNodeNumberPair implements
			Comparable<PieceNodeNumberPair> {
		public FilePiece piece;
		public int numberOfNodes;
		public int numberOfUnfinishedNodes;

		public PieceNodeNumberPair(FilePiece piece, int numberOfNodes,
				int numberOfUnfinishedNodes) {
			this.numberOfNodes = numberOfNodes;
			this.piece = piece;
			this.numberOfUnfinishedNodes = numberOfUnfinishedNodes;
		}

		@Override
		public int compareTo(PieceNodeNumberPair other) {
			if (other.numberOfNodes == this.numberOfNodes)
				return this.numberOfUnfinishedNodes
						- other.numberOfUnfinishedNodes;
			return this.numberOfNodes - other.numberOfNodes;
		}

		public FilePiece getPiece() {
			return piece;
		}
	}

	public void notifyConnectionCompleted(NodeConnection nodeConnection) {
		workingPieceMap.get(nodeConnection.getFilePiece()).remove(
				nodeConnection.getDownNode());
		completedPieceMap.get(nodeConnection.getFilePiece()).add(
				nodeConnection.getDownNode());
	}

	public File getFile() {
		return file;
	}

	public void update() {
		updates++;
		Iterator<NodeConnection> iter = connections.iterator();
		while (iter.hasNext()) {
			if (iter.next().update() == 1) {
				iter.remove();
			}
		}
		Iterator<Node> iterNode = nodes.iterator();
		while (iterNode.hasNext()) {
			if (iterNode.next().update() == 1) {
				iterNode.remove();
			}
		}
	}

	public String getStatusString() {
		StringBuilder nodeStatuses = new StringBuilder();
		for (Node node : nodes) {
			nodeStatuses.append(node.toString() + '\n');
		}

		StringBuilder connectionStatuses = new StringBuilder();
		for (NodeConnection connection : connections) {
			connectionStatuses.append(connection.toString() + '\n');
		}

		return "Cycle #" + updates + '\n' + "Nodes:\nNode\tShare Ratio\t% Complete\tFile Pieces\t\tDown Saturation\tUp Saturation\n" + nodeStatuses.toString() + '\n' +  "Connections:\n"+ "Piece\tUploader\tDownloader\tBandwidth\t% Complete\n" + connectionStatuses.toString() + '\n';
	}

}
